package com.raft.core.log.sequence;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

import javax.annotation.Nonnull;
import javax.annotation.concurrent.NotThreadSafe;

import com.raft.core.log.entry.GroupConfigEntry;

@NotThreadSafe
public class GroupConfigEntryList implements Iterable<GroupConfigEntry> {

    /**
     * 集群配置日志条目链表：为了快速访问到需要的集群配置日志。它把日志中所有和配置相关的条目连接起来，方便遍历
     * <br/>这个集群配置链表记录了初始的集群配置，如果日志快照存在，还可以记录日志快照中的集群配置。集群配置链表理论上作为日志组件的一部分，需要考虑日志快照生成时日志被合并等问题
     */
    private final LinkedList<GroupConfigEntry> entries = new LinkedList<>();

    /**获取最新配置
     * @return
     */
    public GroupConfigEntry getLast() {
        return entries.isEmpty() ? null : entries.getLast();
    }

    /**追加集群配置。在新增或移除节点时通过日志组件间接调用
     * @param entry
     */
    public void add(GroupConfigEntry entry) {
        entries.add(entry);
    }

    /**移除指定索引后的集群配置日志，并返回被移除的第一条配置日志
     * <br/>该方法在追加来自leader节点的日志并且出现日志冲突时被调用。由于raft算法中新的集群配置是作为日志存在的，因此在日志冲突时必须考虑回滚集群配置的可能性。
     * <br/>方法遍历链表并按照索引删除冲突的日志，过程中记录第一个被删除的集群配置日志。之前提到集群配置日志是旧配置机上操作的内容，所以从单条日志可以获取旧配置，
     * <br/>也可以配置操作后的新配置。通过第一个被删除的集群配置日志，可以知道旧的集群配置是什么，然后应用它就行。
     * <br/>如果不存在被删除的集群配置日志，那么就保持原样。
     * <br/>Remove entries whose index is greater than {@code entryIndex}.
     *
     * @param entryIndex entry index
     * @return first removed entry, {@code null} if no entry removed
     */
    public GroupConfigEntry removeAfter(int entryIndex) {
        Iterator<GroupConfigEntry> iterator = entries.iterator();
        GroupConfigEntry firstRemovedEntry = null;
        while (iterator.hasNext()) {
            GroupConfigEntry entry = iterator.next();
            if (entry.getIndex() > entryIndex) {
                if (firstRemovedEntry == null) {
                    firstRemovedEntry = entry;
                }
                iterator.remove();
            }
        }
        return firstRemovedEntry;
    }

    /**指定索引之间的所有配置条目
     * <br/>在推进commitIndex时使用。该方法可以筛选出一定范围内的集群配置日志，通知核心组件这些配置日志被commit了。理论上单服务器变更下，同一时刻最多只能有一个集群配置日志被提交。
     * <br/>核心组件知道集群配置日志被提交后可以顺便做一些处理，比如移除leader节点自身时准备下线
     * @param fromIndex
     * @param toIndex
     * @return
     */
    public List<GroupConfigEntry> subList(int fromIndex, int toIndex) {
        if (fromIndex > toIndex) {
            throw new IllegalArgumentException("from index > to index");
        }
        return entries.stream()
                .filter(e -> e.getIndex() >= fromIndex && e.getIndex() < toIndex)
                .collect(Collectors.toList());
    }

    @Override
    @Nonnull
    public Iterator<GroupConfigEntry> iterator() {
        return entries.iterator();
    }

    @Override
    public String toString() {
        return "GroupConfigEntryList{" + entries + '}';
    }

}
